Análisis Exploratorio de Datos: Torneo de Tenis UNAB 2025¶
Este análisis explora los datos del Torneo de Tenis UNAB 2025, realizado entre el 13 y el 27 de junio de 2025 en el Club San Albano, ubicado en Espora 4920, Burzaco.
Objetivo: realizar una limpieza, exploración y visualización inicial de los resultados para extraer conclusiones relevantes sobre el rendimiento de los jugadores.
📌 Autor: Sebastian Sanchez Bentolila
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
Limpieza y Preparación de Datos¶
En esta sección se realiza la limpieza y transformación de los datos para asegurar su calidad antes del análisis. Esto incluye:
- Revisión de valores nulos.
- Corrección de tipos de datos.
- Renombrado de columnas si es necesario.
- Filtrado de datos irrelevantes o inconsistentes.
# Cargar datos
import requests
from io import StringIO
url1 = "https://raw.githubusercontent.com/Sebastian-Sanchez-Bentolila/data/main/1_Torneo_Tenis_Unab/data/estudiantes.csv"
url2 = "https://raw.githubusercontent.com/Sebastian-Sanchez-Bentolila/data/main/1_Torneo_Tenis_Unab/data/resultados.csv"
estudiantes = pd.read_csv(StringIO(requests.get(url1).text))
resultados = pd.read_csv(StringIO(requests.get(url2).text))
estudiantes.head()
| Nombre | Apellido | Grupo | Carrera | Edad | |
|---|---|---|---|---|---|
| 0 | Sebastian | Sanchez Bentolila | A | Ciencia de Datos | 20 |
| 1 | Franco Nicolas | Tagliaferro | A | Ciencia de Datos | 24 |
| 2 | Humberto Andrés | Coria Avalos | A | CCC. Enseñanza de Matemáticas | 35 |
| 3 | Aarón | Ferreira | A | Ciencia de Datos | 22 |
| 4 | Agustin | Zalazar | A | Administración | 25 |
resultados.head()
| Estudiante_1 | Estudiante_2 | Jornada | Grupo | Resultado | |
|---|---|---|---|---|---|
| 0 | Lautaro Rodriguez | Gaspar Mamani | 1 | B | 0-2 |
| 1 | Micaela López | Milagros Lezcano | 1 | B | 2-0 |
| 2 | Hernán Correa | Romina Fernández | 1 | B | 0-2 |
| 3 | Hernán Correa | Lautaro Rodríguez | 2 | B | 2-1 |
| 4 | Gaspar Mamani | Micaela López | 2 | B | 2-1 |
# Procesar estadísticas
def process_results(df):
# Crear una copia explícita del DataFrame para evitar el warning
df_processed = df.copy()
# Separar los resultados en goles a favor y en contra
df_processed[['GF', 'GC']] = df_processed['Resultado'].str.split('-', expand=True).astype(int)
jugadores = pd.concat([df_processed['Estudiante_1'], df_processed['Estudiante_2']]).unique()
stats = []
for jugador in jugadores:
partidos_jugados = len(df_processed[(df_processed['Estudiante_1'] == jugador) |
(df_processed['Estudiante_2'] == jugador)])
partidos_ganados = len(df_processed[((df_processed['Estudiante_1'] == jugador) &
(df_processed['GF'] > df_processed['GC'])) |
((df_processed['Estudiante_2'] == jugador) &
(df_processed['GC'] > df_processed['GF']))])
partidos_perdidos = partidos_jugados - partidos_ganados
puntos = partidos_ganados * 3
gf = df_processed[df_processed['Estudiante_1'] == jugador]['GF'].sum() + \
df_processed[df_processed['Estudiante_2'] == jugador]['GC'].sum()
gc = df_processed[df_processed['Estudiante_1'] == jugador]['GC'].sum() + \
df_processed[df_processed['Estudiante_2'] == jugador]['GF'].sum()
stats.append({
'Jugador': jugador,
'PJ': partidos_jugados,
'PG': partidos_ganados,
'PP': partidos_perdidos,
'Puntos': puntos,
'GF': gf,
'GC': gc,
'Dif': gf - gc
})
return pd.DataFrame(stats).sort_values('Puntos', ascending=False)
# Procesar los datos sin warnings
stats_a = process_results(resultados[resultados['Grupo'] == 'A'].copy())
stats_b = process_results(resultados[resultados['Grupo'] == 'B'].copy())
# Combinar datos para los gráficos
df_puntos = pd.concat([
stats_a.assign(Grupo='A'),
stats_b.assign(Grupo='B')
])
📊 Visualizaciones Interactivas¶
En esta sección se presentan las visualizaciones interactivas que permiten explorar los datos de manera dinámica para obtener conclusiones rápidas y efectivas.
Distribución de Puntos por Grupo¶
Se analiza la distribución de puntos obtenidos por los jugadores en cada grupo, comparando el rendimiento general y las diferencias entre niveles.
df_puntos = pd.concat([
stats_a.assign(Grupo='A'),
stats_b.assign(Grupo='B')
])
fig1 = px.box(df_puntos, x='Grupo', y='Puntos',
color='Grupo',
color_discrete_map={'A': '#0056b3', 'B': '#007bff'},
title='<b>Distribución de Puntos por Grupo</b><br><i>Comparación entre grupos A y B</i>',
hover_data=['Jugador', 'PJ', 'PG', 'PP'])
fig1.update_layout(
plot_bgcolor='white',
paper_bgcolor='white',
xaxis_title='Grupo',
yaxis_title='Puntos',
showlegend=False,
hovermode='closest'
)
fig1.update_traces(boxmean=True) # Muestra la media
fig1.show()
Relación Games a Favor vs. Games en Contra¶
En esta sección se analiza la relación entre los games ganados y los games perdidos por cada jugador. Esta métrica permite evaluar el rendimiento relativo dentro de los partidos disputados.
fig2 = px.scatter(df_puntos, x='GF', y='GC', color='Grupo',
color_discrete_map={'A': '#0056b3', 'B': '#007bff'},
hover_name='Jugador',
hover_data=['Puntos', 'Dif'],
title='<b>Games a favor vs. games en contra</b><br><i>Jugadores de ambos grupos</i>')
# Línea de igualdad
max_val = max(df_puntos[['GF', 'GC']].max())
fig2.add_trace(
go.Scatter(
x=[0, max_val],
y=[0, max_val],
mode='lines',
line=dict(color='gray', dash='dash'),
name='Línea de igualdad'
)
)
fig2.update_layout(
plot_bgcolor='white',
paper_bgcolor='white',
xaxis_title='Games a favor',
yaxis_title='Games en contra',
legend_title='Grupo'
)
fig2.show()
Promedio de Puntos por Grupo¶
Aquí se calcula y visualiza el promedio de puntos obtenidos por los jugadores en cada grupo. Esto permite comparar el nivel general de desempeño entre los distintos grupos del torneo.
promedios = pd.DataFrame({
'Grupo': ['A', 'B'],
'Puntos': [stats_a['Puntos'].mean(), stats_b['Puntos'].mean()]
})
fig3 = px.bar(promedios, x='Grupo', y='Puntos',
color='Grupo',
color_discrete_map={'A': '#0056b3', 'B': '#007bff'},
text='Puntos',
title='<b>Promedio de puntos por grupo</b>')
fig3.update_traces(texttemplate='%{y:.1f}', textposition='outside')
fig3.update_layout(
plot_bgcolor='white',
paper_bgcolor='white',
xaxis_title='Grupo',
yaxis_title='Puntos promedio',
showlegend=False
)
fig3.show()
Distribución de Jugadores por Carrera¶
Se presenta la distribución de participantes según sus carreras académicas dentro de la universidad, identificando la representatividad de cada disciplina en el torneo.
carrera_counts = estudiantes['Carrera'].value_counts().reset_index()
carrera_counts.columns = ['Carrera', 'Cantidad']
total = carrera_counts['Cantidad'].sum()
carrera_counts['Porcentaje'] = (carrera_counts['Cantidad'] / total * 100).round(1)
fig4 = px.bar(carrera_counts,
x='Cantidad',
y='Carrera',
orientation='h',
color='Cantidad',
color_continuous_scale='Blues',
text='Cantidad',
title='<b>Distribución de estudiantes por carrera</b>')
fig4.update_traces(
texttemplate='%{x} (%{customdata[0]}%)',
customdata=carrera_counts[['Porcentaje']],
textposition='outside'
)
fig4.update_layout(
plot_bgcolor='white',
paper_bgcolor='white',
xaxis_title='Cantidad de estudiantes',
yaxis_title='',
coloraxis_showscale=False,
uniformtext_minsize=8,
uniformtext_mode='hide'
)
fig4.show()
Distribución de Jugadores por Edad¶
En esta sección se analiza la distribución etaria de los jugadores, identificando los rangos de edad más frecuentes y su participación relativa en el torneo.
fig5 = px.histogram(estudiantes,
x='Edad',
nbins=12, # Aumenté el número de bins para más detalle
title='<b>Distribución de Edades de los Participantes</b><br><span style="font-size:14px; color:#555">Torneo de Tenis UNaB 2025</span>',
color_discrete_sequence=['#1a5276'], # Un azul más elegante
marginal='box', # Cambié a box plot para mostrar estadísticas
hover_data=['Carrera', 'Grupo', 'Nombre'],
opacity=0.85, # Transparencia para mejor visualización
template='plotly_white') # Usamos un template limpio
# Personalización avanzada
fig5.update_layout(
plot_bgcolor='rgba(248,249,250,1)', # Fondo muy claro
paper_bgcolor='rgba(248,249,250,1)',
xaxis_title='<b>Edad (años)</b>',
yaxis_title='<b>Cantidad de estudiantes</b>',
bargap=0.15, # Más espacio entre barras
font=dict(
family="Arial",
size=12,
color="#333"
),
hoverlabel=dict(
bgcolor="white",
font_size=12,
font_family="Arial"
),
title_x=0.5, # Título centrado
title_font=dict(size=20),
margin=dict(l=40, r=40, t=80, b=40)
)
fig5.show()
Distribución de Edades por Carrera¶
Aquí se visualiza un boxplot interactivo que muestra la distribución de edades por cada carrera, permitiendo identificar diferencias en la participación según la edad y la disciplina académica.
# Definimos una paleta de colores moderna y accesible
color_palette = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c']
fig6 = px.box(estudiantes,
x='Carrera',
y='Edad',
color='Carrera',
color_discrete_sequence=color_palette,
title='<b>Distribución de Edades por Carrera</b><br><span style="font-size:14px; color:#555">Torneo de Tenis UNaB 2025</span>',
hover_data=['Grupo', 'Nombre'],
points="all", # Muestra todos los puntos
template='plotly_white')
# Personalización avanzada
fig6.update_layout(
plot_bgcolor='rgba(248,249,250,1)',
paper_bgcolor='rgba(248,249,250,1)',
xaxis_title='<b>Carrera Universitaria</b>',
yaxis_title='<b>Edad (años)</b>',
showlegend=False,
font=dict(
family="Arial",
size=12,
color="#333"
),
hoverlabel=dict(
bgcolor="white",
font_size=12,
font_family="Arial"
),
title_x=0.5,
title_font=dict(size=20),
margin=dict(l=50, r=50, t=100, b=80),
height=600 # Altura aumentada para mejor visualización
)
# Personalización de los boxes
fig6.update_traces(
marker=dict(
size=4,
opacity=0.7,
line=dict(width=1, color='DarkSlateGrey')
),
line=dict(width=2),
boxmean=True, # Muestra la media
hovertemplate="<b>Carrera:</b> %{x}<br>" +
"<b>Edad:</b> %{y} años<br>" +
"<extra></extra>"
)
# Rotación de etiquetas y estilo del eje X
fig6.update_xaxes(
tickangle=45,
tickfont=dict(size=12),
showgrid=False
)
# Estilo del eje Y
fig6.update_yaxes(
gridcolor='rgba(200,200,200,0.2)',
showline=True,
linecolor='black'
)
# Añadir línea de promedio general
avg_age = estudiantes['Edad'].mean()
fig6.add_hline(y=avg_age,
line_dash="dot",
line_color="#7f8c8d",
annotation_text=f'Promedio general: {avg_age:.1f} años',
annotation_position="bottom right")
fig6.show()
💾 Guardado de Gráficos Generados¶
En esta etapa se guardan los gráficos generados durante el análisis para su posterior inclusión en informes, presentaciones y documentación del proyecto.
import plotly.express as px
try:
fig1.write_html("output/fig1.html")
fig2.write_html("output/fig2.html")
fig3.write_html("output/fig3.html")
fig4.write_html("output/fig4.html")
fig5.write_html("output/fig5.html")
fig6.write_html("output/fig6.html")
print("Guardado con exito!")
except:
print("Fallo al guardar los gráficos interactivos con Plotly")
Guardado con exito!